package org.tbwork.utility.httprr;

import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.URI;
import java.net.URISyntaxException;
import java.net.URLEncoder;
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;
import java.util.concurrent.TimeUnit;

import org.apache.http.HttpHost;
import org.apache.http.NameValuePair;
import org.apache.http.annotation.NotThreadSafe;
import org.apache.http.client.ClientProtocolException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.methods.HttpRequestBase;
import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.client.protocol.HttpClientContext;
import org.apache.http.cookie.ClientCookie;
import org.apache.http.cookie.Cookie;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.impl.cookie.BasicClientCookie;
import org.apache.http.message.BasicNameValuePair;
import org.jsoup.helper.Validate;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.tbwork.utility.httprr.enums.RequestType;
import org.tbwork.utility.httprr.util.SafeChecker;
import org.tbwork.utility.httprr.util.UrlUtil;

import com.alibaba.fastjson.JSON;


/** Primary implement of HttpRR.<p>
 *  @What HttpRR is a tool to help you send http request and 
 *  receive response easily without caring about inner process
 *  like cookie store, httpClient management, response retrieving, etc. <p>
 *  @Notice This POJO is not thread-safe, which means you need to create instance for each thread 
 *  and void multiple threads access one HttpRR at the same time.<p> 
 *  @How In order to use this tool. You'd better use maven to manage
 *  your project, otherwise you should include jars below to your build path.<BR>
 *  > httpclient-${version}.jar (version >= 4.3.3)<BR>
 *  > logback-core-${version}.jar<BR>
 *  > slf4j-api-${version}.jar<BR>
 *  > logback-classic-${version}.jar<BR>
 *  @author Tommy.Tang<br>
 *  @Date 2014-09-10.
 */
@NotThreadSafe
public class HttpRR {

	private HttpRequestBase httpRequest = null; 
	
	private HttpConfig httpConfig = null;  
	
	private PoolingHttpClientConnectionManager cm = new PoolingHttpClientConnectionManager();
	
	private CloseableHttpClient httpClient = null;
	
	private Map<String, Cookie> cookiesMap = new HashMap<String, Cookie>();
	
	private HttpClientContext proxyAuthenContext = null;
	
	private final static Logger logger = LoggerFactory.getLogger(HttpRR.class);
	
	private final static int HTTP_CLIENT_POOL_SIZE = 5;// Default
	
	private boolean usePool;
	
	public HttpRR() {
		httpConfig = HttpConfig.custom().build();
		cm.setMaxTotal(HTTP_CLIENT_POOL_SIZE);
		usePool = false;
		
	}

	public HttpRR(HttpConfig conf) {
		this.httpConfig = conf;
		cm.setMaxTotal(HTTP_CLIENT_POOL_SIZE);
		cm.setDefaultMaxPerRoute(HTTP_CLIENT_POOL_SIZE);
		usePool = false;
	}
	
	/**
	 * @param conf http configuration
	 * @param poolSize the size of http-client pool
	 */
	public HttpRR(HttpConfig conf, boolean usePool) {
		this.httpConfig = conf;  
		this.usePool = usePool;
	}
	
	/**
	 * @param conf http configuration
	 * @param poolSize the size of http-client pool
	 */
	public HttpRR(HttpConfig conf, boolean usePool, int poolSize) {
		this.httpConfig = conf; 
		this.usePool = usePool;
		cm.setMaxTotal(poolSize); 
		cm.setDefaultMaxPerRoute(poolSize);
	}
 
	/** This method is used to send your request to the target server and get the response.<br>
	 * 
	 * @param request instance of Request
	 * @return instance of Response
	 * @throws Exception
	 */
	public Response execute(Request request) {
    
		Validate.notEmpty(request.getUrl(),"Please specify a URL for your request.");

		prepareCookie(request);

		httpRequest = createHttpRequest(request);

		Response response = new Response();
		
		CloseableHttpResponse httpResponse = MultiExecuteSendRequest(httpRequest); 
		
		if (httpResponse == null) {
			response.setResponse(null);
		} else {
			response.setResponse(httpResponse);
		} 
 
		if (httpConfig.isAutoCookieRecord()) {
			updateCookieStore(response);
		}

		return response;
	}


	
 
	/** In the form of: key1=value1;key2=value2;
	 * @return cookie in the form of string.
	 */
	public String getCookiesString() {
		List<Cookie> list = getCookieList();
		String result = "";
		for (Cookie item : list) {
			result = result + item.getName() + "=" + item.getValue() + ";";
		} 
		return result;
	}
 
	public List<Cookie> getCookieList() {
		List<Cookie> list = new ArrayList<Cookie>();
		Set<String> keySet = cookiesMap.keySet();
		for (String item : keySet) {
			list.add(cookiesMap.get(item));
		}
		return list;
	}

 
	/**
	 * Clear all cookies store in the HttpRR. 
	 */
	public void clearCookie() {
		cookiesMap.clear();
	}

	public void addCookie(Cookie cookie) {
		String key = cookie.getDomain() + "/" + cookie.getName();
		cookiesMap.put(key, cookie);
	}

	/** Add cookie by list.
	 * @param cookieList
	 */
	public void addCookieList(List<Cookie> cookieList) {
		for (Cookie item : cookieList) {
			addCookie(item);
		}
	}

	/*-------------------Functions below are private.-------------------------*/
	private void prepareCookie(Request request)
	{
		// check and load cookie from header
		checkAndLoadCookieFromRequestHeader(request);
		
		// load cookie from request's cookie map field
		loadCookieFromRequestCookie(request);
		
		// set cookie
		if(this.getCookiesString() != null &&  !this.getCookiesString().isEmpty())
			request.setHeader("Cookie", this.getCookiesString());
	}
	
	private void loadCookieFromRequestCookie(Request request)
	{
		cookiesMap.putAll(request.getCookieMap());
	}
	
	private void checkAndLoadCookieFromRequestHeader(Request request) { 
		if(SafeChecker.isEmpty(request.getHeader("Cookie"))) 
				return;
		String cookieString = request.getHeader("Cookie").trim();
		if (SafeChecker.notEmpty(cookieString)) {
			try {
                  String[] cookieArray = cookieString.split(";"); 
				  for (String item : cookieArray) { 
						String [] keyValue = item.trim().split("=");
						if (SafeChecker.notEmpty(keyValue) && keyValue.length == 2) {
								HttpCookie tempCookie = new HttpCookie(keyValue[0], keyValue[1]);
								addCookie(tempCookie);
						} else
							throw new Exception();
				  } 
			} catch (Exception e) { 
				throw new IllegalArgumentException("The Cookie string you add by 'request.addHeader()' is in bad form. Right form looks like: \"a=1;b=2;c=3\". Please check and try again.");
			}
		}
	}

	
	private CloseableHttpResponse MultiExecuteSendRequest(HttpUriRequest request)
	{
		int count = httpConfig.getRetryTimeCount();
		CloseableHttpResponse tempResponse = null;
		while(count>0 && tempResponse == null)
		{
			tempResponse = excuteSendRequest(request);				
			count --;
		}
		return tempResponse;
	}
	
	private CloseableHttpResponse excuteSendRequest(HttpUriRequest request)  { 
		   if(logger.isDebugEnabled())
			   logger.debug("Request details. \n Url: {}", request.getURI());
			CloseableHttpResponse response = null;
			try {
				if(usePool){
					httpClient = getHttpClient();
					
				}
				else{
					if(httpClient == null){
						synchronized(HttpRR.class){
							if(httpClient == null)
							{ 
									httpClient = HttpClients.createDefault();
							} 
						}
					}
				}
				
				System.out.println(JSON.toJSONString(request));
				if(httpConfig.isProxyAuthenEnabled()) 
					response = httpClient.execute(request, httpConfig.getProxyAuthenContext());  
				else
					response = httpClient.execute(request);  
				 
			} 
			catch( ClientProtocolException e)
			{
				logger.error(String.format("Http protocol error. Detailed message: %s", e.getMessage())); 
			}
			catch( IOException e)
			{
				logger.error(String.format("A problem occurred, or the connection was aborted. Detailed message: %s", e.getMessage()));  
				e.printStackTrace();
			} 
			finally{  
			}
			return response;
	} 
	
	private void updateCookieStore(Response response) {
		List<Cookie> cookieList = getCookiesList(response);
		for (Cookie item : cookieList) {
			String name = item.getName();
			String domain = item.getDomain();
			cookiesMap.put(domain + "/" + name, item);
		}
	}

	private List<String> getCookiesStringList(Response response) {
		List<String> result = new ArrayList<String>();
		org.apache.http.Header[] headers = response.getHeaders("Set-Cookie");
		for (org.apache.http.Header item : headers) {
			String text = item.toString().replace("Set-Cookie: ", "");
			result.add(text);
		}
		return result;
	}
	 
	
	private void initialize(Request request) 
	{
		createHttpRequest(request); 
	}

	private HttpRequestBase createHttpRequest(Request request) 
	{ 
		HttpRequestBase tmpHttpRequest = request.getRequestType() ==  RequestType.POST ? new HttpPost(request.getUrl()) : new  HttpGet(request.getUrl());
		try
		{  
			tmpHttpRequest.setConfig(httpConfig.getRequestConfig());
			//host check, get host domain name from request url if it is not specified.
			boolean hostSpecified = false; 
			Set<Entry<String,String>> entrySet = request.getHeaders().entrySet();
			for(Entry<String,String> item: entrySet)
			{ 
				tmpHttpRequest.addHeader(item.getKey(), item.getValue());
			} 
			entrySet = request.getSubtmits().entrySet();  
			List <NameValuePair> nvps = new ArrayList <NameValuePair>(); 
			String currentUrl = request.getUrl();
			if(RequestType.GET.equals(request.getRequestType()))
			{ 
				for(Entry<String,String> item: entrySet)
				{
					currentUrl += formParameterIntoUrl(currentUrl,item.getKey(), item.getValue()); 
				}

				if(logger.isDebugEnabled())
					 logger.debug("Final Url : {}", currentUrl);
				tmpHttpRequest.setURI(new URI(currentUrl));
			}
			else
			{
				for(Entry<String,String> item: entrySet)
				{
					nvps.add(new BasicNameValuePair(item.getKey(), item.getValue()));
				}
			} 
		  	if(!nvps.isEmpty())
		  		((HttpPost)tmpHttpRequest).setEntity(new UrlEncodedFormEntity(nvps));
		  	if( request.getPostEntity() !=null)
		  	{
		  		((HttpPost)tmpHttpRequest).setEntity(request.getPostEntity());
		  	}
		}
		catch( UnsupportedEncodingException e)
		{
			logger.error("Create httpRequest failed. Inner message:"+ e.getMessage());
		}
		catch(Exception e)
		{
			logger.error("Create httpRequest failed. Inner message:"+ e.getMessage());
		}
		
	    return tmpHttpRequest;
	}
	
	
	private String formParameterIntoUrl(String currentUrl, String paramName, String paramValue)
	{ 
		 try{
			 String resultUrl ="";
			 paramName = URLEncoder.encode(paramName, "utf-8");
			 paramValue = URLEncoder.encode(paramValue, "utf-8");
			 if(currentUrl.indexOf("?")==-1)
			 {
			     resultUrl ="?"+paramName+"="+paramValue;
			 }
			 else
			 {
				 resultUrl ="&"+paramName+"="+paramValue; 
			 }
			 return resultUrl;
		 }
		 catch(Exception e)
		 {
			 logger.error("The named encoding is not supported.");
			 return currentUrl;
		 } 
	}			

	
	private List<Cookie> getCookiesList(Response response) {
		List<String> cookieStringList = getCookiesStringList(response);
		List<Cookie> result = new ArrayList<Cookie>();
		for (String item : cookieStringList) {
			String[] fields = item.split(";");
			String cookieName = "";
			String cookieValue = "";
			BasicClientCookie temp = null;
			if (fields.length > 0) {
				String[] pairs = fields[0].split("=");
				if (pairs != null && pairs.length == 2) {
					temp = new BasicClientCookie(pairs[0], pairs[1]);
				}
			} 
			Map<String, String> key_value = new HashMap<String, String>();
			for (String strItem : fields) {
				if (!strItem.isEmpty()) {
					String[] pairs = strItem.split("="); 
					if (pairs != null && pairs.length == 2) {
						key_value.put(pairs[0], pairs[1]);
						if (pairs[0].toLowerCase().equals("domain")) {
							temp.setDomain(pairs[1]);
						} else if (pairs[0].toLowerCase().equals("path")) {
							temp.setPath(pairs[1]);
						} else if (pairs[0].toLowerCase().equals("expiry")) {
							temp.setExpiryDate(new Date(pairs[1]));
						} else if (pairs[0].toLowerCase().equals("version")) {
							temp.setVersion(Integer.parseInt(pairs[1]));
						}
					}
				}
			}
			result.add(temp);
		}
		return result;
	}

	
	private CloseableHttpClient getHttpClient(){  
        return HttpClients.custom().setConnectionManager(cm).build();
    } 
	
}
